package tp.badugi.server;

import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.Random;

import tp.badugi.net.GameStatus;
import tp.badugi.net.ServerNetworkingChannel;
import tp.badugi.net.exceptions.NameTakenException;
import tp.badugi.net.exceptions.ServerCouldNotCompleteRequestException;
import tp.badugi.net.exceptions.TableFullException;
import static tp.badugi.server.Server.*;

public class Table implements Runnable {
	
	private static final int DELAY_BEFORE_GAME_STARTS = 10000;
	private static final int DELAY_BEFORE_GAME_RESTARTS = 30000;

	private Server server;
	private GamePlayController gameController;
	private String tableName;
	private int maxPlayers;
	private long startingChips;
	private List<Player> players;
	private List<Player> playersToBeRemoved;
	private long potSize;
	private boolean gameStarted;
	private int playerWithButton = 0;
	private long smallBlind = 50;
	private long bigBlind = 100;
	private long betAmount;
	
	public Table(Server server, String tableName, int maxPlayers, long startingChips) {
		this.server = server;
		this.tableName = tableName;
		this.maxPlayers = maxPlayers;
		this.startingChips = startingChips;
		this.players = new ArrayList<Player>();
		this.playersToBeRemoved = new ArrayList<Player>();
		gameController = new GamePlayController(this);
	}
	
	public synchronized Player addPlayer(String playerName, ServerNetworkingChannel netChannel)
			throws TableFullException, NameTakenException, ServerCouldNotCompleteRequestException
	{
		if (gameStarted)
			throw new ServerCouldNotCompleteRequestException("The game has started.");
		int playerCount = players.size();
		if (playerCount == maxPlayers) {
			throw new TableFullException("The table is full.");
		}
		for (Player existingPlayer : players) {
			String existingPlayerName = existingPlayer.getName();
			if (playerName.equals(existingPlayerName)) {
				throw new NameTakenException("This name has been taken.");
			}
		}
		Player player = 
				new Player(this, netChannel, playerName, startingChips);
		players.add(player);
		announcePlayerJoined(player);
		return player;
	}

	public void addToPot(long amount) {
		potSize += amount;
	}
	
	public void announceNewPlayerInAction(Player playerInAction) {
		// Prepare information for sending to players
		String[] playerNames = new String[players.size()];
		int playerIndex = 0;
		for (int i = 0; i < players.size(); i++) {
			playerNames[i] = players.get(i).getName();
			if (playerNames[i].equals(playerInAction.getName()))
				playerIndex = i;
		}
		
		// Notify each player
		for (Player player : players) {
			player.notifyPlayerInActionChanged(playerIndex, playerNames);
		}
	}
	
	public void announcePlayerDisconnected(Player disconnectedPlayer) {
		// Notify each player except the disconnected player
		for (Player player : players) {
			if (player != disconnectedPlayer) {
				player.notifyPlayerDisconnected(disconnectedPlayer.getName());
			}
		}
	}
	
	public void announcePlayerReconnected(Player reconnectedPlayer) {
		// Notify each player except the reconnected player
		for (Player player : players) {
			if (player != reconnectedPlayer) {
				player.notifyPlayerReconnected(reconnectedPlayer.getName());
			}
		}
	}
	
	public void announcePlayerReplacedCards(Player playerWithCards, int cardCount) {
		// Notify each player
		for (Player player : players) {
			player.notifyPlayerReplacedCards(playerWithCards.getName(), cardCount);
		}
	}
	
	public void announcePlayersAction(Player playerInAction, Action action) {
		// Prepare information for sending to players
		GameStatus gameStatus = getGameStatus();
		int playerIndex;
		for (playerIndex = 0; playerIndex < gameStatus.playerNames.length; playerIndex++) {
			if (gameStatus.playerNames[playerIndex].equals(playerInAction.getName()))
				break;
		}
		
		// Notify each player
		for (Player player : players) {
			player.notifyPlayerPerformedAction(
					playerIndex, action.toString(), action.amount, gameStatus);
		}
	}	

	public void announceRoundChange(int bettingRound) {
		// Notify each player
		for (Player player : players) {
			player.notifyBettingRoundChanged(bettingRound);
		}
	}

	public void announceWinnersRevealed(List<Player> winners) {
		// Prepare information for sending to players
		int[] winnerIndexes = new int[winners.size()];
		String[] playerNames = new String[players.size()];
		List<List<Card>> revealedCards = new ArrayList<List<Card>>();
		long[] updatedChipCounts = new long[players.size()];
		int winnerCounter = 0;
		for (int i = 0; i < players.size(); i++) {
			Player player = players.get(i);
			if (winners.contains(player)) {
				winnerIndexes[winnerCounter] = i;
				winnerCounter++;
			}
			playerNames[i] = player.getName();
			updatedChipCounts[i] = player.getChipCount();
			List<Card> playersCards = player.getCards();
			revealedCards.add(playersCards);
		}
		
		// Notify each player
		for (Player player : players) {
			player.notifyWinnerRevealed(
					winnerIndexes, playerNames, revealedCards, updatedChipCounts);
		}
	}

	public void clear() {
		potSize = 0;
		for (Player player : players) {
			player.setPotContribution(0);
			player.discardCards();
		}
	}

	public long getBetAmount() {
		return betAmount;
	}
	
	public long getBigBlind() {
		return bigBlind;
	}
	
	public GameStatus getGameStatus() {
		GameStatus gameStatus = new GameStatus();
		gameStatus.numberOfPlayers = players.size();
		gameStatus.playerStatuses = new boolean[players.size()];
		for (int i = 0; i < players.size(); i++) {
			gameStatus.playerStatuses[i] = players.get(i).isConnected();
		}
		gameStatus.playerNames = new String[players.size()];
		for (int i = 0; i < players.size(); i++) {
			gameStatus.playerNames[i] = players.get(i).getName();
		}
		gameStatus.playerChipCounts = new long[players.size()];
		for (int i = 0; i < players.size(); i++) {
			gameStatus.playerChipCounts[i] = players.get(i).getChipCount();
		}
		gameStatus.playerPotContributions = new long[players.size()];
		for (int i = 0; i < players.size(); i++) {
			gameStatus.playerPotContributions[i] = players.get(i).getPotContribution();
		}
		gameStatus.playerWithDealerButton = playerWithButton;
		gameStatus.smallBlind = smallBlind;
		gameStatus.bigBlind = bigBlind;
		gameStatus.bettingRound = gameController.getBettingRound();
		Player playerInAction = gameController.getPlayerInAction();
		if (playerInAction != null) {
			int playerIndex;
			for (playerIndex = 0; playerIndex < gameStatus.playerNames.length; playerIndex++) {
				if (gameStatus.playerNames[playerIndex].equals(playerInAction.getName()))
					break;
			}
			gameStatus.playerInAction = playerIndex;
		}
		else {
			gameStatus.playerInAction = -1;
		}
		gameStatus.betAmount = betAmount;
		gameStatus.potSize = potSize;
		gameStatus.tableName = tableName;
		
		return gameStatus;
	}
	
	public String getName() {
		return tableName;
	}
	
	public long getPotSize() {
		return potSize;
	}
	
	public long getSmallBlind() {
		return smallBlind;
	}

	public void removePlayer(Player player) {
		
		// If the game has started then mark player for deletion and wait for appropriate moment
		if (gameStarted) {
			playersToBeRemoved.add(player);
			
			// If all players left then remove table from list immediately.
			if (playersToBeRemoved.size() == players.size()) {
				server.removeTable(this);
			}
		}
		
		// If the game hasn't started then delete player right away
		else {
			players.remove(player);
			if (VERBOSE) {
				String time = server.getTime();
				String playerName = player.getName();
				System.out.println(
						time + ": Player " + playerName + " left table \"" + tableName + "\"");
			}
		}
	}
	
	@Override
	public void run() {
		gameStarted = false;
		
		try {
			// Only run as long as there are players
			while(players.size() > 0) {
				
				// Wait for players to connect
				if (!gameStarted && players.size() == maxPlayers) {
					
					// Start game
					gameStarted = true;
					
					// Randomly select dealer (don't notify clients until later)
					Random rand = new Random();
					playerWithButton = rand.nextInt(maxPlayers);
				}
				else if (gameStarted) {
					// Only allow players who have a minimum of big blind to continue playing
					List<Player> nonBankruptPlayers = new ArrayList<Player>(players);
					for (Player player : players) {
						if (player.getChipCount() < bigBlind) {
							nonBankruptPlayers.remove(player);
						}
					}
					
					// Only play if there is more than one player with money
					if (nonBankruptPlayers.size() > 1) {
						// Give players time to prepare
						announceGameAboutToStart(DELAY_BEFORE_GAME_STARTS / 1000);
						Thread.sleep(DELAY_BEFORE_GAME_STARTS);
						
						// Determine who to pass the button to
						if (playerWithButton + 1 >= players.size()) {
							playerWithButton = 0;
						}
						else {
							playerWithButton++;
						}
						announcePlayerWithButtonChanged();
						
						// Oversee deal
						gameController.overseeDeal(nonBankruptPlayers, playerWithButton);
					}
					
					// Otherwise restart
					else if (nonBankruptPlayers.size() == 1 && players.size() != 1) {
						for (Player player : players) {
							player.setChips(startingChips);
						}
						
						// Give players time
						announceGameAboutToRestart(DELAY_BEFORE_GAME_RESTARTS / 1000);
						Thread.sleep(DELAY_BEFORE_GAME_RESTARTS);
					}
					
					// Remove any players that left during the game
					removeMarkedPlayers();
				}
				Thread.sleep(1000);
			}
		}
		catch (InterruptedException e) { 
			System.err.println("TODO: Notify clients that table has encountered exception.");
		}
		
		// If all players leave then delete table
		server.removeTable(this);
	}
	
	public void setBetAmount(long amount) {
		betAmount = amount;
	}
	
	private void announceGameAboutToRestart(int timeUntilRestart) {
		// Notify each player
		for (Player player : players) {
			player.notifyGameAboutToRestart(timeUntilRestart);
		}
	}
	
	private void announceGameAboutToStart(int timeUntilStart) {
		// Notify each player
		GameStatus gameStatus = getGameStatus();
		for (Player player : players) {
			player.notifyGameAboutToStart(timeUntilStart, gameStatus);
		}
	}

	private void announcePlayerJoined(Player newPlayer) {
		// Prepare information for sending to players
		GameStatus gameStatus = getGameStatus();
		int playerIndex;
		for (playerIndex = 0; playerIndex < gameStatus.playerNames.length; playerIndex++) {
			if (gameStatus.playerNames[playerIndex].equals(newPlayer.getName()))
				break;
		}
		
		// Notify each player except the player that joined
		for (Player player : players) {
			if (player != newPlayer) {
				player.notifyPlayerJoined(playerIndex, gameStatus);
			}
		}
	}

	private void announcePlayerLeft(Player playerThatLeft) {
		// Prepare information for sending to players
		GameStatus gameStatus = getGameStatus();
		
		// Notify each player except the player that left
		for (Player player : players) {
			if (player != playerThatLeft) {
				player.notifyPlayerLeft(playerThatLeft.getName(), gameStatus);
			}
		}
	}

	private void announcePlayerWithButtonChanged() {
		// Prepare information for sending to players
		String[] playerNames = new String[players.size()];
		for (int i = 0; i < players.size(); i++) {
			playerNames[i] = players.get(i).getName();
		}
		
		// Notify each player
		for (Player player : players) {
			player.notifyPlayerWithButtonChanged(playerWithButton, playerNames);
		}
	}

	private void removeMarkedPlayers() {
		while (playersToBeRemoved.size() > 0) {
			Player player = playersToBeRemoved.remove(0);
			int playerIndex = players.indexOf(player);
			if (playerWithButton == playerIndex) {
				playerWithButton = 
						playerWithButton - 1 < 0 ? players.size() - 2 : playerWithButton - 1;
				announcePlayerWithButtonChanged();
			}
			players.remove(playerIndex);
			if (VERBOSE) {
				String time = server.getTime();
				String playerName = player.getName();
				System.out.println(
						time + ": Player " + playerName + " left table \"" + tableName + "\"");
			}
			announcePlayerLeft(player);
		}
	}
	
}
